class LexerFileException(Exception):
    def __init__(self, msg:str):
        self.msg = msg
    def __str__(self):
        return self.msg
    
def check_dict(dict_obj, key_name, value_type, value_len, err_location):
    if key_name not in dict_obj:
        raise LexerFileException('"%s" not exist! location: %s'%(key_name, err_location))
    if value_type != None:
        if type(dict_obj[key_name]) != value_type:
            raise LexerFileException('type error! expect:%s, location: %s'%(value_type, err_location))
    if value_len != None:
        if len(dict_obj[key_name]) != value_len:
            raise LexerFileException('length error! expect:%d, location: %s'%(value_len, err_location))

def check_type(obj, exp_type, err_location):
    if type(obj) != exp_type:
        raise LexerFileException('type error! expect:%s, location: %s'%(exp_type, err_location))

def check_list(list_obj, length, types, err_location):
    if type(list_obj) != list:
        raise LexerFileException('type error! expect:%s, location: %s'%(list, err_location))
    if len != None:
        if len(list_obj) != length:
            raise LexerFileException('length error! expect:%d, location: %s'%(length, err_location))
    if types != None:
        for i in range(0, len(types)):
            if type(list_obj[i]) != types[i]:
                raise LexerFileException('type error! expect:%s, location: %s.[%d]'%(types[i], err_location, i))

def check_length(obj, length, obj_type, err_location):
    if len(obj) != length:
        raise LexerFileException('length error! expect:%s, location:%s'%(length, err_location))
    if obj_type != None:
        if type(obj) != obj_type:
            raise LexerFileException('type error! location:%s'%(err_location))

def check_length_range(obj, min_len, max_len, obj_type, err_location):
    '''Args:
    obj: 待检测的对象
    min_len: 最小值（含），None表示不检查
    max_len: 最大值（含），None表示不检查
    obj_type: obj的类型，None表示不检查
    err_location: 如果不符合要求，错误信息中要表示的出错位置'''
    if min_len != None:
        if len(obj) < min_len:
            raise LexerFileException('length too short! location: %s'%(err_location))
    if max_len != None:
        if len(obj) > max_len:
            raise LexerFileException('length too long! location: %s'%(err_location))
    if obj_type != None:
        if type(obj) != obj_type:
            raise LexerFileException('type error! location: %s'%(err_location))

def check_exist(key, obj, err_location):
    if key not in obj:
        raise LexerFileException('"%s" is not exist! location: %s'%(key, err_location))

class LexerChecker:
    def __init__(self, dfa_cond_func_names:list):
        self.dfa_cond_func_names = set(dfa_cond_func_names)

    def __check_token_item(self, name:str, item:list, dfa_names:set):
        '''检查一个token条目
        
        Args:
            name: token_name
            item: 对应的内容
        Return: is_dfa'''
        if name.isidentifier() == False:
            raise LexerFileException('invalid token name: %s'%(name))
        check_list(item, 3, [str, str, int], '"tokens"."%s"'%(name))
        token_type = item[0]
        if token_type == 'char':
            check_length(item[1], 1, None, '"tokens"."%s".[1]'%(name))
        elif token_type == 'string':
            check_length_range(item[1], 2, None, None, '"tokens"."%s".[1]'%(name))
        elif token_type == 'dfa':
            check_length_range(item[1], 1, None, None, '"tokens"."%s".[1]'%(name))
            check_exist(item[1], dfa_names, '"tokens"."%s".[1]'%(name))
        else:
            raise LexerFileException('unknow token type: "%s", location: "tokens"."%s".[0]'%(token_type, name))
        if item[2] < 0:
            raise LexerFileException('priority must larger than 0! location: "tokens"."%s".[2]'%(name))

    def __check_cond_items(self, items, dfa_name:str, state_name:str, all_state_names:set):
        '''Args:
            item: 要检查的一个条目
            dfa_name: 所属dfa的名字
            state_name: 所属状态的名字
            all_state_names: 所属dfa包含的所有状态的名字'''
        has_default_cond = False
        for idx in range(0, len(items)):
            item = items[idx]
            check_dict(item, 'cond', list, None, '"dfa"."%s"."%s".[%d]."cond"'%(dfa_name, state_name, idx))
            check_dict(item, 'action', list, None, '"dfa"."%s"."%s".[%d]."action"'%(dfa_name, state_name, idx))
            check_dict(item, 'next', str, None, '"dfa"."%s"."%s".[%d]."next"'%(dfa_name, state_name, idx))
            # cond
            if len(item['cond']) == 0:
                has_default_cond = True
            else:
                check_length(item['cond'], 2, None, '"dfa"."%s"."%s".[%d]."cond"'%(dfa_name, state_name, idx))
                cond_type = item['cond'][0]
                cond_value = item['cond'][1]
                if cond_type == 'chars':
                    check_length_range(cond_value, 1, None, str, '"dfa"."%s"."%s".[%d]."cond"'%(dfa_name, state_name, idx))
                elif cond_type == 'range':
                    check_list(cond_value, 2, None, '"dfa"."%s"."%s".[%d]."cond".[1]'%(dfa_name, state_name, idx))
                    check_length(cond_value[0], 1, str, '"dfa"."%s"."%s".[%d]."cond".[1]"'%(dfa_name, state_name, idx))
                    check_length(cond_value[1], 1, str, '"dfa"."%s"."%s".[%d]."cond".[1]"'%(dfa_name, state_name, idx))
                elif cond_type == 'function':
                    if cond_value not in self.dfa_cond_func_names:
                        raise LexerFileException('unknow condition function: "%s", location: "dfa"."%s"."%s"[%d]'%(cond_value, dfa_name, state_name, idx))
                else:
                    raise LexerFileException('unknow condition type: "%s", location: "dfa"."%s"."%s"[%d]'%(cond_type, dfa_name, state_name, idx))
            # action
            legal_actions = ['push', 'read', 'continue']
            for action_name in item['action']:
                if action_name not in legal_actions:
                    raise LexerFileException('unknow action: "%s", location: "dfa"."%s"."%s"[%d]'%(action_name, dfa_name, state_name, idx))
            # next
            next_name = item['next']
            if next_name not in all_state_names:
                raise LexerFileException('unknow state name: "%s", location: "dfa"."%s"."%s"[%d]'%(next_name, dfa_name, state_name, idx))
        if has_default_cond == False:
            raise LexerFileException('can not find default empty condition! location: "dfa"."%s"."%s"'%(dfa_name, state_name))
        if state_name == 'Start' and len(items) == 1:
            raise LexerFileException('"Start" state must has at least 2 condtions! location: "dfa"."%s"."Start"'%(dfa_name))

    def __check_dfa(self, dfa_name:str, dfa_info:dict):
        if dfa_name.isidentifier() == False:
            raise LexerFileException('invalid dfa name: %s'%(dfa_name))
        has_start_state = False
        all_state_names = set(['Start', 'End', 'Error'])
        for state_name in dfa_info:
            all_state_names.add(state_name)
        for state_name in dfa_info:
            state_info = dfa_info[state_name]
            if state_name == 'Start':
                has_start_state = True
            if state_name.isidentifier() == False:
                raise LexerFileException('invalid state name: "dfa"."%s"."%s"'%(dfa_name, state_name))
            self.__check_cond_items(state_info, dfa_name, state_name, all_state_names)
        if has_start_state == False:
            raise LexerFileException('can not find "Start" state in "dfa"."%s"'%(dfa_name))

    def __check_empty_char(self, char):
        if len(char) == 1 and char != '\\':
            return True
        if len(char) == 2 and char[0] == '\\':
            return True
        return False

    def check(self, lexer_info):
        '''检查lexer_info内容的合法性
        
        Args:
            lexer_info: [IN/OUT] 用json解析后的内容'''
        dfa_names = set()
        if 'dfa' in lexer_info:
            for dfa_name in lexer_info['dfa']:
                dfa_info = lexer_info['dfa'][dfa_name]
                self.__check_dfa(dfa_name, dfa_info)
                dfa_names.add(dfa_name)
        if 'blank_chars' in lexer_info:
            blank_chars = lexer_info['blank_chars']
            for idx in range(0, len(blank_chars)):
                char = blank_chars[idx]
                if self.__check_empty_char(char) == False:
                    raise LexerFileException('illegal blank char: "%s", location: "blank_char".[%d]'%(char, idx))
        else:
            lexer_info['blank_chars'] = [' ', '\\n', '\\t', '\\r']
        check_dict(lexer_info, 'tokens', dict, None, '"{}"')
        if len(lexer_info['tokens']) == 0:
            raise LexerFileException('there is not token item in "tokens"!')
        for token_name in lexer_info['tokens']:
            self.__check_token_item(token_name, lexer_info['tokens'][token_name], dfa_names)
